import java.lang.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;


public class URSserver {
	public MulticastDB db;
	/* Information in the database will be in the following order
	[expiration time] [URS-Identifier] [start time] [channel IP] [channel port] [unicast IP] 
	[unicast port] [channel scope] [Geographic CN] [Lat] [Long] [network type] 
	[source IP]	[stream type] [preferred application] [CLI argument] [mime-type]
	 */
	public static String myPort = "defaultMyPort";
	public static String parentIP = null;
	public static String parentPort = null;
	public static String msdID = "defaultMSDID";
	public static String msdIDHash = "defaultHash";
	public static String msdIP = "defaultMSDIP";
	public static String msdPort = "defaultMSDPort";
	public static boolean isRoot = false;
	//Deprecated fields:
	public static String msdCmcastIP = "128.227.205.222";
	public static String msdCmcastPort = "18010";
	public static String msdPmcastIP = "128.227.74.56";
	public static String msdPmcastPort = "18000";
	public static String msdPmcastSourceIP = "128.227.74.01";
	public static String msdLocalIP = "128.227.74.56";
	public static String msdLocalPort = "17050";
	public static String msdIGMPVersion = "IGMPv3";
	//configuration values
	public String serverNameOrIP = "localhost";
	public static int listenPort = 30000;
	private final int listenTimeoutInterval = 1000; //milliseconds
	
	//class variables
	public boolean alive = true;
	public boolean closeClientThreads = false;
	public BufferedReader stdIn;
	
	public static void main(String[] args)
	{
		//Need passed in through commmand line:
		//myPort msdID msdIP msdPort
		//If not the parent, also need parentIP parentPort
		try
		{
			myPort = args[0];
			msdID = args[1];
			msdIDHash = Helpers.getMD5Hash(msdID);
			msdIP = args[2];
			msdPort = args[3];
			listenPort=Integer.parseInt(myPort);
		}
		catch(Exception e)
		{
			System.err.println("Command line should contain the following parameters:");
			System.err.println("<URS Port> <MSD Name> <MSD IP> <MSD Port> ");
			System.err.println("And if not in the root domain also:<Parent URS IP> <Parent URS Port>");
			System.exit(-1);
		}
		try
		{
			parentIP = args[4];
			parentPort = args[5];
		}
		catch (Exception e)
		{
			isRoot=true;
		}
		URSserver me = new URSserver();
		//me.testServer();
		me.startServer();
	}
    /*private void testServer()
    {
    	/*
    	 * check 0x03 3 US-ASCII test
    	 * register 0x01 17 US-ASCII [expirationtime] URS-Identifier [channelIP] [channelport] [unicastIP] [unicastport] [channelscope] [GeographicCN] [Lat] [Long] [networktype] [sourceIP] [streamtype] [preferredapplication] [CLIargument] [mime-type]
    	 * query 0x01 2 US-ASCII URS-Identifier
    	 * request 0x01 1 US-ASCII designated
    	 * resync 0x02 0
    	 *
    	stdIn = new BufferedReader(
                new InputStreamReader(System.in)); //get reference to terminal input
    	Socket testSocket = new Socket();
        db=new MulticastDB("TestURS_db.csv");
    	URSClientWorker testWorker = new URSClientWorker(this, testSocket);
    	while(true)
    		{
    		try {
				if(stdIn.ready()){
					testWorker.processMessage(stdIn.readLine());
					
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
    	}
    }
    */
	private void startServer()
	{
		stdIn = new BufferedReader(
                new InputStreamReader(System.in)); //get reference to terminal input
    	
		if(listenPort == -1)
		{
			printError("ERROR: server port not specified properly.");
			printError("Please input command like the following ex:");
			printError("<My Port> <MSD Name> <MSD IP> <MSD Port> ");
			printError("And if not in the root domain also:<Parent URS IP> <Parent URS Port>");
			System.exit(1);
		}
		if(listenPort < 1 || listenPort > 65535) // port must be in range 1-65535, preferably > 49151
        {
        	printError("ERROR: Port number must be in range 1-65535. Supplied: " + listenPort + ".");
        	System.exit(1);
        }
			
		ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(listenPort);
        } catch (IOException e) {
        	printError("0x001 Error encountered.");
            printError("  Description: Server could not listen on port: " + listenPort + ".");
            System.exit(1);
        }
        
        print("Type \"exit\" and hit enter at any time to close the server.");
        print("Listening for new connections on port: " + listenPort + ".");
        db=new MulticastDB(msdID + "URS_db.csv");
		Queue<Thread> myWorkerThreads = new LinkedList<Thread>();
        while(alive)
        {
	        try { //set timeout interval for non-blocking listen
	        	serverSocket.setSoTimeout(listenTimeoutInterval); 
	        }
	        catch(SocketException e){
	        	printError("Could not set listen timeout interval on socket accept.");
	        }
	        	
	        try{ //try to accept any waiting connections
	        	Socket clientSocket = serverSocket.accept();
	        	print("Received new connection.");
    			URSClientWorker w = new URSClientWorker(this, clientSocket);
                Thread t = new Thread(w);
                t.start(); //spawn a new thread to handle the communication
                myWorkerThreads.add(t);
	        	}
	        catch (SocketTimeoutException ste) {	
	        	//this is OK, just means we did not receive a connection yet
	        	//this is where intermediate server worker code should go
	        	
				try {
					if(stdIn.ready() && stdIn.readLine().toLowerCase().startsWith("exit"))
					{
						alive = false;
						closeClientThreads = true;
						print("Server closed from console.");
					}
				} catch (IOException e) {		}
	        	
	        }
	        catch (IOException e) {
	        	printError("0x004 Error encountered.");
	        	printError("IOException encountered while trying to listen for connections.");
	        	
	        }
        }   
        
        try {
			serverSocket.close();
		} catch (IOException e) {
			printError("Could not close server socket.");
		}
        try {
			stdIn.close();
		} catch (IOException e) {
			printError("Could not close standard input.");
		}
		
		while(myWorkerThreads.size() > 0)
        {
        	Thread t = myWorkerThreads.remove();
        	if(t != null)
        	{
        		while(t.isAlive())
        		{ //sit here and wait 
        		}
        	}
        }
        print("All ClientWorker threads closed.");
		
		System.exit(0);
        
	}
	
	public void printError(String errorMessage)
	{
		//can put logic in here to print to log file if needed
		System.err.println("ERROR: " + errorMessage);
	}

	public void print(String message)
	{
		//can put logic in here to print to log file if needed
		System.out.println(message);
	}
	
}


class URSClientWorker implements Runnable
{
	private URSserver ursServer;
	private Socket clientSocket; //used to communicate with connected entity
	private int noMessagesCloseTimeout = 300; //this many seconds of inactivity will force a close of the thread
	private BufferedReader in = null; //to listen
	private PrintWriter out = null; //to talk
	private boolean receivedBye = false;
	
	//Constructor
	URSClientWorker(URSserver ursServer, Socket clientSocket)
	{
	    this.ursServer = ursServer;
		this.clientSocket = clientSocket;
	}
	  
	public void run()
	{ //this is where the thread starts on creation
		print("Communication thread started.");
	    
	    try{
			in = new BufferedReader(new 
					InputStreamReader(clientSocket.getInputStream()));
			out = new PrintWriter(clientSocket.getOutputStream(), true);
	    } catch (IOException e) {
	    	printError("Could not get I/O streams");
	    	System.exit(-1);
	    }

	    //boolean toldToKill = false;
	    boolean messageWaiting = false;
	    long timeoutTimeInSeconds = System.currentTimeMillis()/1000 + noMessagesCloseTimeout;
	    while(true)
	    {
	    	try {
	    		messageWaiting = false;
				messageWaiting = in.ready();
			} catch (IOException e1) {
				printError("Could not check if in was ready.");
			}
	    	
	    	if(messageWaiting)
	    	{
				String messageIn = null;
				try {
					messageIn = in.readLine();
				} catch (IOException e) {
					printError("Could not read message from socket.");
				}
				
				print("Incoming message: " + messageIn);
				processMessage(messageIn);
				//reset timeout since we received a message
				timeoutTimeInSeconds = System.currentTimeMillis()/1000 + noMessagesCloseTimeout;
	    	}
	    	
			if(ursServer.closeClientThreads)
			{
				print("Thread closed by parent.");
				out.println("BYE");
				break; //creator signaled to close
			}
			if(receivedBye) 
				break; //BYE message received, so we will close
			if(System.currentTimeMillis()/1000 > timeoutTimeInSeconds)
			{
				print("Thread timed out waiting for message (" + noMessagesCloseTimeout + " seconds.)");
				out.println("BYE");
				break; //it has been too long since we have received a message
			}
			//sleep for a 1/4 second and check again for messages
			try { Thread.sleep(250);} catch (InterruptedException e) {e.printStackTrace();}

	    }
	    
	    //close connections
	    try{
	    	in.close(); //input stream
	    } catch (IOException e) {
	    	printError("Could not close socket input stream.");
	    }
	    
	    out.close(); //output stream
	    
	    try{
	    	clientSocket.close(); //client socket
	    } catch (IOException e) {
	    	printError("Could not close socket.");
	    }
	}
	
	protected void processMessage(String messageIn)
	{
		//this is where all the logic will go for processing messages
		ArrayList<String> splitMessage = new ArrayList<String>();
		StringTokenizer st = new StringTokenizer(messageIn);
		while(st.hasMoreTokens()){
			splitMessage.add(st.nextToken());
		}
		String response = null;
		BaseMessage message = new BaseMessage();
		if(!message.parseBaseMessage(messageIn))
		{
			response = messageIn + " ??? not formed right";
		}
		else
		{
			try
			{
				switch(message.type) {
			     case UNKNOWN:
			       response = messageIn + "??? Message type not recognized";
			       break;
			     case CHECK:
			    	splitMessage.get(4); //check should have 5 fields
					if(ursServer.db.recordExists(splitMessage.get(4)))
					{
		    			response="CHECK-RESPONSE 0x03 1 false";
					}
		    		else
		    		{
		    			response="CHECK-RESPONSE 0x03 1 true";
		    		}
					break;
			     case REGISTER:
			    	 splitMessage.get(19); //register should have 20 fields
			    	 if(ursServer.db.recordExists(splitMessage.get(5)))
			    	 {
			    		 response="REGISTER-STATUS 0x03 1 false";
			    	 }
			    	 else
			    	 {
			    		 String registerMessage = "";
			    		 for (int i=4; i<splitMessage.size(); i++)
			    		 {
			    			 //Trim off the first 4 fields that don't belong in the database
			    			 registerMessage+=splitMessage.get(i) + " ";
			    			 if (i==5)
			    			 {
			    				 //Add the registration time to the database after the keyword
			    				 Calendar cal = Calendar.getInstance();
			    				 registerMessage+=cal.getTime().toString().replace(" ", "") + " ";
			    			 }
			    		 }
			    		 ursServer.db.createRecord(registerMessage);
			    		 response="REGISTER-STATUS 0x03 1 true";
			    	 }
			    	 break;
			     case QUERY:
			    	 splitMessage.get(4); //query should have 5 fields
			    	 if(ursServer.db.recordExists(splitMessage.get(4)))
			    	 {
			    		String[] record=ursServer.db.getRecord(splitMessage.get(4));
			    		//0[expiration time] 1[URS-Identifier] 2[start time] 3[channel IP] 4[channel port] 5[unicast IP] 6[unicast port] 7[channel scope] 8[Geographic CN] 9[Lat] 10[Long] 11[network type] 12[source IP]	13[stream type] 14[preferred application] 15[CLI argument] 16[mime-type]
			    		
			    		//[channel IP] [channel port] [Geographic CN] [Lat] [Long] [unicast IP] [unicast port] [channel scope] [URS-Identifier] [expiration time] [network type] [source IP] [stream type] [preferred application] [CLI argument] [mime-type][\n]
			    		response="QUERY-RESPONSE 0x03 17 US_ASCII " + record[3] + 
			    			" " + record[4] + " " + record[8] + " " + record[9] + " " + 
			    			record[10] + " " + record[5] + " " + record[6] + " " + record[7] + 
			    			" " + record[1] + " " + record[0] + " " + record[11] + " " + 
			    			record[12] + " " + record[13] + " " + record[14] + " " + record[15] + 
			    			" " + record[16]; //query-response type 2
			    	 }
			    	 else
			    	 {
			    		 response="QUERY-RESPONSE 0x03 1 null"; //query-response type 1
			    	 }
			    	 break;
			     case REQUEST:
			    	 splitMessage.get(3); //request should have 4 fields
			    	 if(splitMessage.get(3).equalsIgnoreCase("mcast_param"))
			    	 {
			    		 //Message came from MSD
			    		 response="REQUEST-RESPONSE 0x09 11 mcast_param " + URSserver.msdCmcastIP + " " + 
			    		 	URSserver.msdCmcastPort + " " + URSserver.msdPmcastIP + " " + 
			    		 	URSserver.msdPmcastPort + " " + URSserver.msdPmcastSourceIP + " " + 
			    		 	URSserver.msdLocalIP + " " + URSserver.msdLocalPort + " " + 
			    		 	URSserver.msdIP + " " + URSserver.msdPort + " " + URSserver.msdIGMPVersion;
			    	 }
			    	 else
			    	 {
			    		 if (splitMessage.get(1).equals("0x01")) //Message came from Client
			    		 {
			    			 response="REQUEST-RESPONSE 0x03 3 designated " + URSserver.msdIP + " " + URSserver.msdPort;
			    		 }
			    		 else //Message came from another URS
			    		 {
			    			 response="REQUEST-RESPONSE 0x05 3 designated " + URSserver.msdIP + " " + URSserver.msdPort;
			    		 }
			    	 }
			    	 break;
			     case BYE:
			    	 splitMessage.get(2); //bye should have 3 fields
			    	 if (splitMessage.get(1).equals("0x01")) //From Client
			    	 {
			    		response="BYE 0x03 0";
			    	 }
			    	 else if (splitMessage.get(1).equals("0x02")) //From MSD
			    	 {
			    		response="BYE 0x09 0";
			    	 }
			    	 else if (splitMessage.get(1).equals("0x05")) //From URS
			    	 {
			    		 response="BYE 0x05 0";
			    	 }
			    	 try {
						clientSocket.close();
					} catch (IOException e) {
						printError("Failed to close socket");
					}
					break;
			     case UNICAST_UPDATE:
			    	 splitMessage.get(5); //unicast-update should have 6 fields
			    	 //[unicast-update] [0x02] [3] [idhash] [msd IP] [msd port]
			    	 try
			    	 {
				    	 String newMsdHash = splitMessage.get(3);
				    	 String newMsdIP = splitMessage.get(4);
				    	 String newMsdPort = splitMessage.get(5);
				    	 URSserver.msdIDHash = newMsdHash;
				    	 URSserver.msdIP = newMsdIP;
				    	 URSserver.msdPort = newMsdPort;
				    	 response="UNICAST-UPDATE-STATUS 0x09 1 true";
			    	 }
			    	 catch(Exception e)
			    	 {
			    		 //Unicast-update-status is unlikely to return false
			    		 response="UNICAST-UPDATE-STATUS 0x09 1 false";
			    	 }
			    	 break;
			     case RESYNC:
			    	 splitMessage.get(2);  //resync should have 3 fields
			    	 String outMessage = "REQUEST 0x05 1 designated";
			    	 String serverResponse = "";
			    	 PrintWriter outToServer = null;
			         BufferedReader inFromServer = null;
			    	 try 
			    	 {
			    		//Set up a client to communicate with parent URS
			    		//resync should not be sent to any server in the parent domain, but would return false in that circumstance
						Socket outSocket = new Socket(URSserver.parentIP, Integer.parseInt(URSserver.parentPort));
						outToServer = new PrintWriter(outSocket.getOutputStream(), true);
			            inFromServer = new BufferedReader(new InputStreamReader(outSocket.getInputStream()));
			            outToServer.println(outMessage);
			            serverResponse = inFromServer.readLine();
			            outToServer.close();
			    		inFromServer.close();
			    		outSocket.close();
			    		ArrayList<String> splitResponse = new ArrayList<String>();
			            StringTokenizer st2 = new StringTokenizer(serverResponse);
			            while(st2.hasMoreTokens())
			            {
			            	splitResponse.add(st2.nextToken());
			            }
			            //splitResponse="REQUEST-RESPONSE 0x05 3 designated " + URSserver.msdIP + " " + URSserver.msdPort;
			            try
			            {
			            	response="RESYNC-STATUS 0x09 3 true " + splitResponse.get(4) + " " + splitResponse.get(5);
			            }
			            catch(Exception e)
			            {
			            	printError("Response from other URS contains incorrect number of fields");
			            }
			            //  [resync-status] [0x09] [3] [true] [msd IP] [msd port][\n]
			    	 } 
			    	 catch (Exception e) 
			    	 {
						//printError("Connection to parent URS failed");
						//Should also be false for "bad data," definition TBD
						response="RESYNC-STATUS 0x09 1 false";
			    	 } 
			    	 break;
				}
		    }
			catch(IndexOutOfBoundsException e)
			{
				//e.printStackTrace();
				response=("Message improperly formed.  Ensure all required arguments are present and try again.");
			}
		}
		
		print("Response: " + response);
		out.println(response); //send back a response
			
	}
	
	public void printError(String errorMessage)
	{
		//can put logic in here to print to log file if needed
		System.err.println("ERROR: " + errorMessage);
	}

	public void print(String message)
	{
		//can put logic in here to print to log file if needed
		System.out.println(message);
	} 
}